import { resolve } from "path";
import fs from 'fs';
import dotenv from 'dotenv';
import { createRequire } from 'module';
import webpack from "webpack";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import CopyWebpackPlugin from "copy-webpack-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import TerserPlugin from "terser-webpack-plugin";
import ClientPlugin from "./clientPlugin";
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';

import { getAliases, getEntry, getHtmlTemplate, getAssetPath } from "./paths";
import type { CSSOptions, EnvType, WebpackConfigType } from "./types";

const { DefinePlugin, IgnorePlugin } = webpack as unknown as typeof import('webpack');

const config = ({
  commandName,
  env = {
    NODE_ENV: process.env.NODE_ENV || "",
  },
  css = {},
  base = "/",
  outputDir = "dist",
  assetsDir = "assets",
  envPrefix = 'APP_',
  assetsInlineLimit = 4096,
  sourcemap,
  reactRefresh = true,
  pages,
}: {
  commandName: string,
  env?: EnvType,
  css?: CSSOptions,
  base?: string,
  outputDir?: string,
  assetsDir?: string,
  envPrefix?: string | string[],
  assetsInlineLimit?: number,
  sourcemap?: boolean | 'inline' | 'hidden'
  reactRefresh?: boolean,
  pages?: Record<string, string | { entry: string; template?: string; filename?: string; }>
  | Array<{ name: string; entry: string; html?: Record<string, any> }>,
}): WebpackConfigType => {
  const nodeRequire = createRequire(import.meta.url);
  const isServe = commandName === "serve";
  const isHot =
    env.NODE_ENV === "development" ||
    env.WEBPACK_WATCH ||
    env.WEBPACK_SERVE ||
    isServe;
  const isBuild =
    env.NODE_ENV === "production" || env.WEBPACK_BUILD || !isServe;
  const isDevSourcemap = !!(css?.devSourcemap);

  // load .env files like vite: .env, .env.local, .env.[mode], .env.[mode].local
  const mode = isServe ? 'development' : 'production';
  const envFiles = [
    `.env.${mode}.local`,
    `.env.${mode}`,
    `.env.local`,
    `.env`,
  ];
  for (const file of envFiles) {
    if (fs.existsSync(file)) {
      dotenv.config({ path: file, override: true });
    }
  }
  const prefixes = Array.isArray(envPrefix) ? envPrefix as string[] : [envPrefix as string];
  const filteredEnv: Record<string, string> = {};
  for (const [k, v] of Object.entries(process.env)) {
    if (prefixes.some(p => k.startsWith(p))) {
      filteredEnv[k] = String(v);
    }
  }
  const userCssModules = css?.modules;
  const userPostcss = css?.postcss;
  const cssLoader = [
    isHot && nodeRequire.resolve("style-loader"),
    isBuild && {
      loader: MiniCssExtractPlugin.loader,
      options: {
        esModule: false,
      },
    },
    {
      loader: nodeRequire.resolve("css-loader"),
      options: {
        modules: userCssModules === false ? false : {
          auto: /\.module\.\w+$/i,
          localIdentName: "_[local]_[hash:base64:8]",
          exportLocalsConvention: "camelCase",
          ...(typeof userCssModules === 'object' ? userCssModules : {}),
        },
        sourceMap: isHot || isDevSourcemap,
      },
    },
    {
      loader: nodeRequire.resolve("postcss-loader"),
      options: {
        sourceMap: isHot || isDevSourcemap,
        postcssOptions: typeof userPostcss === 'string'
          ? nodeRequire(resolve(userPostcss))
          : (typeof userPostcss === 'object' ? userPostcss : {
            plugins: [
              nodeRequire.resolve("postcss-flexbugs-fixes"),
              [
                nodeRequire.resolve("postcss-preset-env"),
                {
                  autoprefixer: { flexbox: "no-2009" },
                  stage: 3,
                },
              ],
              nodeRequire.resolve("postcss-normalize"),
            ],
          }),
      },
    },
  ];

  const normalizePath = (basePath: string, isOutput = false) => {
    if (!basePath) return "/";
    if (basePath === 'auto') return basePath;
    if (isOutput && basePath === './') return basePath;
    if (/^https?:\/\//i.test(basePath)) return basePath.endsWith('/') ? basePath : basePath + '/';
    if (basePath.startsWith('./') || basePath.startsWith('../') || !basePath.startsWith('/')) return basePath.replace(/^(\.\/|\.\.\/)+/, '/');

    const withLead = basePath.startsWith('/') ? basePath : '/' + basePath;
    return withLead.endsWith('/') ? withLead : withLead + '/';
  };

  const normalizedPublicPath = normalizePath(base);
  const outputPublicPath = normalizePath(base, true);

  const baseConfig: WebpackConfigType = {
    devServer: {
      hot: true,
      compress: true,
      devMiddleware: {
        publicPath: normalizedPublicPath,
      },
      historyApiFallback: {
        disableDotRule: true,
      },
      allowedHosts: "all",
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "*",
        "Access-Control-Allow-Headers": "*",
      },
      static: {
        directory: resolve("public"),
        publicPath: normalizedPublicPath
      },
      client: {
        progress: false,
        overlay: true
      }
    },
    infrastructureLogging: {
      level: 'warn'
    },
    entry: getEntry(),
    output: {
      path: resolve(outputDir),
      publicPath: outputPublicPath,
      filename: getAssetPath(assetsDir, '[name].[contenthash:8].js'),
      chunkFilename: getAssetPath(assetsDir, '[name].[contenthash:8].js'),
      assetModuleFilename: getAssetPath(assetsDir, '[name].[contenthash:8].[ext]'),
      pathinfo: false,
    },
    mode: isHot ? "development" : "production",
    devtool: isHot ? 'cheap-module-source-map' : (sourcemap === true ? 'source-map' : sourcemap === 'inline' ? 'inline-source-map' : sourcemap === 'hidden' ? 'hidden-source-map' : false),
    stats: {
      colors: true,
      chunks: false,
      chunkModules: false,
      chunkOrigins: false,
      modules: false,
      moduleTrace: false,
      source: false,
      warnings: true,

      // 打包后的列表
      assets: isBuild,
      entrypoints: isBuild,
      children: false,
    },
    resolve: {
      modules: ["node_modules"],
      extensions: [
        ".js",
        ".jsx",
        ".ts",
        ".tsx",
        ".json",
        ".less",
        ".css",
        ".mjs",
        ".svelte",
        ".vue",
        ".gql",
        ".graphql",
      ],
      alias: getAliases(),
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          use: [
            {
              loader: "esbuild-loader",
              options: {
                loader: "jsx",
                jsx: 'automatic',
                target: "es2015",
              },
            },
          ],
          exclude: /node_modules/,
        },
        {
          test: /\.ts$/,
          use: [
            {
              loader: "esbuild-loader",
              options: {
                loader: "ts",
              },
            },
          ],
          exclude: /node_modules/,
        },
        {
          test: /\.tsx?$/,
          use: [
            {
              loader: "esbuild-loader",
              options: {
                loader: "tsx",
                jsx: 'automatic',
                target: "es2015",
              },
            },
          ],
          exclude: /node_modules/,
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf|pdf|docx|doc|xlsx|xls|pptx|ppt|mp4|mp3|wav|ogg|webm|zip|rar|7z|tar|gz|bz2)$/,
          type: "asset/resource",
        },
        {
          test: /\.(png|jpg|jpeg|gif|svg|ico|webp|avif)$/,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              maxSize: assetsInlineLimit
            }
          }
        },
        {
          test: /\.(txt|json|xml|csv|yaml|yml|toml)$/,
          type: 'asset/source',
        },
        {
          test: /\.css$/,
          use: cssLoader.filter(Boolean),
        },
        {
          test: /\.less$/,
          use: [
            ...cssLoader,
            {
              loader: "less-loader",
              options: {
                sourceMap: isHot || isDevSourcemap,
                lessOptions: {
                  javascriptEnabled: true,
                },
                implementation: nodeRequire.resolve("less"),
                ...((css?.preprocessorOptions?.less) ||
                  {}),
              },
            },
          ].filter(Boolean),
        },
        {
          test: /\.s[ac]ss$/i,
          use: [
            ...cssLoader,
            {
              loader: "sass-loader",
              options: {
                implementation: nodeRequire.resolve("sass"),
                sassOptions: {
                  includePaths: ["node_modules"],
                },
              },
            },
          ].filter(Boolean),
        },
      ].filter(Boolean),
    },
    plugins: [
      new DefinePlugin({
        "process.env": JSON.stringify(filteredEnv),
      }),
      new IgnorePlugin({
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      }),
      new HtmlWebpackPlugin({
        meta: false,
        inject: true,
        template: getHtmlTemplate(),
        minify: isHot ? false : {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      }),
      (isHot && reactRefresh) && new ReactRefreshWebpackPlugin({ overlay: false }),
      !isBuild && new ClientPlugin(normalizedPublicPath),
      isBuild && new CleanWebpackPlugin(),
      isBuild &&
      new CopyWebpackPlugin({
        patterns: [
          {
            from: "./public",
            globOptions: {
              ignore: pages && Object.keys(pages).length > 0 ? ["**/*.html"] : ["**/index.html"],
            },
          },
        ],
      }),
      isBuild &&
      new MiniCssExtractPlugin({
        filename: getAssetPath(assetsDir, '[name].[contenthash:8].css'),
        chunkFilename: getAssetPath(assetsDir, '[name].[contenthash:8].css'),
        ignoreOrder: true,
      }),
    ].filter(Boolean),
    optimization: {
      nodeEnv: process.env.NODE_ENV,
      runtimeChunk: 'single',
      minimize: isBuild,
      minimizer: isBuild
        ? [
          new TerserPlugin({
            terserOptions: {
              compress: {
                ecma: 5,
                comparisons: false,
                inline: 2,
              },
              mangle: {
                safari10: true,
              },
              output: {
                ecma: 5,
                comments: false,
                ascii_only: true,
              },
            },
            extractComments: false,
          }),
          new CssMinimizerPlugin(),
        ]
        : [],
      splitChunks: isBuild
        ? {
          chunks: "all",
          cacheGroups: {
            defaultVendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10,
              chunks: "all",
            },
            default: {
              minChunks: 2,
              chunks: "all",
              priority: -20,
              reuseExistingChunk: true,
            },
          },
        }
        : false,
      moduleIds: 'deterministic',
      chunkIds: 'deterministic'
    },
    performance: {
      hints: false,
    },
    target: "web",
    cache: {
      type: 'filesystem'
    },
  };

  // pages support
  const normPages = (() => {
    if (!pages) return null;
    if (Array.isArray(pages)) return pages;
    const arr: Array<{ name: string; entry: string; html?: Record<string, any> }> = [];
    for (const [name, value] of Object.entries(pages)) {
      if (typeof value === 'string') arr.push({ name, entry: value });
      else if (value && typeof value === 'object') {
        const { entry, template, filename } = value as any;
        const html: any = {};
        if (template) html.template = template;
        if (filename) html.filename = filename;
        arr.push({ name, entry, html });
      }
    }
    return arr;
  })();

  if (normPages && normPages.length > 0) {
    const multiEntry: Record<string, string> = {};
    const htmlPlugins: any[] = [];
    const minifyOpt = isHot ? false : {
      removeComments: true,
      collapseWhitespace: true,
      removeRedundantAttributes: true,
      useShortDoctype: true,
      removeEmptyAttributes: true,
      removeStyleLinkTypeAttributes: true,
      keepClosingSlash: true,
      minifyJS: true,
      minifyCSS: true,
      minifyURLs: true,
    } as any;
    const hasExplicitIndex = normPages.some((p: any) => p?.html?.filename === 'index.html');
    normPages.forEach((p: any, i: number) => {
      const name = p.name;
      const entry = p.entry;
      const html = p.html || {};
      multiEntry[name] = resolve(entry);
      const filename = html.filename || (!hasExplicitIndex && i === 0 ? 'index.html' : `${name}.html`);
      htmlPlugins.push(new HtmlWebpackPlugin({
        filename,
        chunks: [name],
        inject: true,
        template: html.template || getHtmlTemplate(),
        minify: minifyOpt,
        ...html
      }));
    });
    baseConfig.entry = multiEntry as any;
    // replace default html plugin: keep others
    baseConfig.plugins = (baseConfig.plugins || []).filter(p => !(p as any)?.constructor?.name?.includes('HtmlWebpackPlugin')).concat(htmlPlugins);
  }

  return baseConfig;
};

export default config;
